Cache注解

  • 开启注解,以后再方法上加个注解就可以自动把查询到的数据放到缓存中。
  • 如果是用xml配置文件创建CacheManager的话,添加以下配置即开启cache注解:

    <!--假设上面注册了spring的CacheManager,id为cacheManager-->
    <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>
    <!--还有一个属性key-generator,即key生成策略,默认是SimpleKeyGenerator-->
    
  • 如果是Java configuration的话:

@Configuration
@ComponentScan(basePackages = "com.haien.sping.cache.service")
@EnableCaching(proxyTargetClass = true) //效果同<cache:annotation-driven/>,开启注解
public class AnnotationCacheConfig {
    @Bean
    public CacheManager cacheManager(){
        try {
            //2.
            net.sf.ehcache.CacheManager ehcacheCacheManager=new net.sf.ehcache.CacheManager(
                    new ClassPathResource("ehcache.xml").getInputStream()); //1.
            //3.
            EhCacheCacheManager cacheCacheManager=new EhCacheCacheManager(ehcacheCacheManager);

            return cacheCacheManager;
        } catch (IOException e) { //getInputStream()抛出的
            throw new RuntimeException(e);
        }
    }
}
  • 如果想要设置KeyGenerator的话,可以实现CachingConfigurer:
@Configuration
@ComponentScan(basePackages = "com.haien.sping.cache.service")
@EnableCaching(proxyTargetClass = true)
public class AnnotCacheConfWithKeyGenertor implements CachingConfigurer {
    @Bean
    public CacheManager cacheManager() {
        try {
            net.sf.ehcache.CacheManager ehcacheCacheManager=new net.sf.ehcache.CacheManager(
                    new ClassPathResource("ehcache.xml").getInputStream()
            );
            EhCacheCacheManager cacheCacheManager=new EhCacheCacheManager(ehcacheCacheManager);

            return cacheCacheManager;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //默认为此
    @Bean
    public KeyGenerator keyGenerator() {
        return new SimpleKeyGenerator();
    }
}
  • 然后使用cache注解:所有注解都可以放在类或方法上,如果放在类上则会应用到所有方法上;并且,如果将注解放在接口类的方法上,则所有实现类在重写方法时都会继承这个注解。
@Service
public class UserService {
    //假设这是数据库
    Set<User> users=new HashSet<User>();

    //调用该方法时,会把user.id作为key,放入value指定的缓存,可以指定多个缓存,
      如,value={"user1","user2"};#表示调用参数
    @CachePut(value = "user",key = "#user.id") 
    public User save(User user){
        users.add(user);
        return user;
    }

    //调用方法前先从缓存中读取,没有再调用方法并把数据放进缓存
    @Cacheable(value = "user",key="#id") 
    public User findById(final Long id){
        System.out.println("cache miss,invoke find by id,id="+id);
        for(User user:users){
            if(user.getId().equals(id))
                return user;
        }

        return null;
    }

    @CacheEvict(value = "user",key = "#user.id") //移除指定key的数据
    public void delete(User user){
        users.remove(user);
    }

    @CacheEvict(value = "user",allEntries = true) //移除所有数据
    public void deleteAll(){
        users.clear();
    }
}
  • @CachePut:应用在写数据的方法上,在调用方法前并不会先检查缓存中是否已有该值,即有也会被覆盖。
public @interface CachePut {
    String[] value(); //缓存的名字,可以把数据放入多个缓存;支持SpEL表达式

    String key() default ""; //缓存key,不指定将使用默认的KeyGenerator生成;SpEL

    String condition() default ""; //能否放入缓存的条件,在方法调用前后都会判断;
                                     SpEL

    String unless() default ""; //返回false则能放入缓存;SpEL
}
  • @Cacheable: 应用在读数据的方法上,即可缓存的方法上,如查找;调用前会先从缓存读取,没有再调用方法,并把数据放入缓存。
public @interface Cacheable {
    String[] value(); //同@CachePut
    String key() default ""; //同@CachePut
    String condition() default ""; //方法调用前判断是否能从缓存拿;
                                     执行方法后是否能把数据放入缓存的条件
    String unless() default ""; //返回false则能放入缓存
}
  • @CacheEvict:应用在移除数据的方法上。
public @interface CacheEvict {
    String[] value(); //同@CachePut

    String key() default ""; //同@CachePut

    String condition() default ""; //调用前还是调用后判断取决于beforeInvocation

    boolean allEntries() default false; //是否移除所有数据

    boolean beforeInvocation() default false;//是调用方法前还是调用后移除
}

注解参数所用的SpEL

  • target:被注解方法所在的类

  • 通过这些数据我们可以实现比较复杂的缓存逻辑了。

条件缓存

  • 利用注解的condition或unless属性,进行有条件的缓存。
  • @Cacheable:在方法调用前判断condition,返回true则先从缓存中拿:
@Cacheable(value = "user", key = "#id", condition = "#id lt 10") //id<10?
public User conditionFindById(final Long id)

//condition示例:以下代码位于UserService类中
//判断执行前是否要先删缓存
@CacheEvict(value = "user",key = "#user.id",
    condition = "#root.target.canEvict(#root.caches[0],#user.id,#user.username)",
    beforeInvocation = true)
public void conditionalUpdate(User user){
    users.remove(user);
    users.add(user);
}

/**
 * @Author haien
 * @Description 判断是否需要删除缓存
 * @Date 2019/3/24
 * @Param [userCache名为“user”的cache, id, username]
 * @return boolean
 **/
public boolean canEvict(Cache userCache,Long id,String username) {
    //根据id查出条目
    User user=userCache.get(id,User.class);
    //查不到则不用删缓存
    if(user==null)
        return false;

    //查到了但username不是指定的那个也不用删除
    return !user.getUsername().equals(username);
}
  • 以上方法缺点在于需要将缓存判断方法也暴露出去,而且缓存代码和业务代码混在一起,所以把canEvict()移到一个Helper静态类中更好(不过代码中没实现):
@CacheEvict(value = "user", key = "#user.id", 
    condition = "T(com.haien.service.UserCacheHelper)
        .canEvict(#root.caches[0], #user.id, #user.username)", 
    beforeInvocation = true)
public void conditionUpdate(User user){}
  • @CachePut:以下condition需要result值,所以只在方法执行后才判断,返回true则放入缓存:
//ne:应该是not equal的意思
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")
public User conditionSave(final User user) 
  • 如下注解只在方法执行后才判断unless,返回false才放入缓存:
@CachePut(value = "user", key = "#user.id", 
    unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user) 

//message是result的属性,contains是SpEL的方法
@Cacheable(value = "spittleCache", 
    unless = "#result.message.contains('NoCache')") 
Spittle findOne(long id);
  • @CacheEvict:beforeInvocation=false表示在方法执行后才删除缓存,且condition要返回true才删除:
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, 
    condition = "#result.username ne 'zhang'")
public User conditionDelete(final User user) 

@Caching

  • 用于组合多个注解使用。
public @interface Caching {
    Cacheable[] cacheable() default {}; //声明多个@Cacheable
    CachePut[] put() default {}; //声明多个@CachePut
    CacheEvict[] evict() default {}; //声明多个@CacheEvict
}
  • 比如,新增用户成功后,我们要分别以id、username和email为key同时存储value都为user实例的三条数据,这时可以使用该注解:
@Caching(
    put = {
            @CachePut(value = "user", key = "#user.id"),
            @CachePut(value = "user", key = "#user.username"),
            @CachePut(value = "user", key = "#user.email")
    }
)
public User save(User user) {}
  • 其实最好是id-user,然后username-id、email-id,保证user只存一份。

运行流程

  • 使用@Caching组合多个注解或在同个方法加多个注解时,运行流程如下:
  1. 首先执行@CacheEvict清空缓存(如果beforeInvocation=true且condition通过)。
  2. 接着收集@Cacheable(如果condition通过,即方法执行后允许写入缓存,且key对应的数据不在缓存);
  3. 然后收集@CachePut(如果condition通过)。
  4. 全部收集到cachePutRequests(表示缓存中缺失数据,有放入缓存的需求)。
  5. 如果cachePutRequests为空,那么@Cacheable时将先查找缓存,否则直接开始执行方法。
  6. 如果缓存中找不到数据,那么开始执行方法,把结果放入result。
  7. 执行cachePutRequests,把result中的数据写入缓存(如果unless返回false)。
  8. 执行@CacheEvict(如果beforeInvocation=false 且 condition 通过)。
  • 其实也就是有beforeInvocation或condition这种需要在方法执行前先检查的注解先被处理,然后再执行方法,最后再处理有beforeInvocation或condition的注解。
  • 由以上2、3步,只要注解中有@CachePut,cachePutRequests就不可能为空,@Cacheable就不会去缓存中取。

解决@Cacheable和@CachePut不能同时用的问题

  • 只要有@CachePut在,@Cacheable就不会从缓存拿的问题其实只要改掉spring中CacheAspectSupport类某个方法的判断条件即可。
  • 我们在自己的test包下建包:org.springframework.cache.intercepter(CacheAspectSupport原本就在spring框架中的该包下),自己一模一样的CacheAspectSupport类,只修改其中判断能否从缓存查东西的条件(本来是只要有@CachePut就不查,现在改成只要有2Cacheable就查):
package org.springframework.cache.interceptor

public abstract class CacheAspectSupport implements InitializingBean {

    private Object execute(Invoker invoker, CacheOperationContexts contexts) {
        ...

        Collection<CacheOperationContext> cacheOperationContexts =
                contexts.get(CacheableOperation.class);

        /* 原代码要求cacheOperationContexts为空且cachePutRequests也为空,即没有@CachePut时才从缓存拿
        if (cachePutRequests.isEmpty() && cacheOperationContexts.isEmpty()) {
            result = findCachedResult(cacheOperationContexts);
        }*/
        //现在只查cacheOperationContexts,且相反,不为空才查缓存,也就是只要有@Cacheable就查缓存
        if (!cacheOperationContexts.isEmpty()) {
            result = findCachedResult(cacheOperationContexts);
        }

        ...
    }
}
  • 但是只改上面这个类只是做了第一步,后面还要做什么配置就不知道了。

自定义组合注解

  • 上面那个@Caching用在方法上会显得有点乱,此时我们可以把它定义成一个简洁的注解:
@Caching(
    put = {
            @CachePut(value = "user", key = "#user.id"),
            @CachePut(value = "user", key = "#user.username"),
            @CachePut(value = "user", key = "#user.email")
    }
)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserSaveCache {

}

KeyGenerator

  • KeyGenerator接口:如果在Cache注解上没有指定key的话,如@CachePut(value=”user”),会调用KeyGenerator自动生成一个key。
public interface KeyGenerator {
    Object generate(Object target, Method method, Object... params);
}
  • 默认实现:
  • DefaultKeyGenerator:
@Override
public Object generate(Object target, Method method, Object... params) {
    //如果参数为空,就不知道key是个什么特定值了
    if (params.length == 0) {
        return SimpleKey.EMPTY;
    }
    //如果只有一个参数,就使用参数作为key,即传入User实例名为User参数
      的方法,key默认为“user”
    if (params.length == 1 && params[0] != null) { 
        return params[0];
    }
    return new SimpleKey(params);
}
  • SimpleKeyGenerator:Spring4之后默认使用的。
  • 自定义KeyGenerator:然后用<cache:annotation-driven key-generator=””/>指定,或实现CachingConfiguration接口时重写方法设置进去。

使用xml代替缓存注解

  • 提供元素如下:

缓存模糊匹配